你是新創公司 Imager 底下的前端工程師,Imager 提供的服務非常簡單,就是能在網頁瀏覽各式各樣的圖片,網頁已經上線並正常的運作中,我們來看看他的模樣。
看起來一切正常,但此時 PM 卻出現在你的身旁,告訴你:
網頁慢的原因我們可以簡單的歸納一下:
首先我們先剔除後面兩點,因為這兩個是我們無法控制的。所以原因肯定是出在前面兩者之間,而經過了初步的診斷,Imager 也就一個首頁和圖片列表而已,打包後的大小實在是沒什麼問題,那麼原因就是⋯
至於 Imager 網頁上需要下載的資源,只能是「圖片」了吧!檢查了一下發現,首頁的頁面上的列表直接把 100 張圖片都放上去了,這就代表著使用者一進頁面就會開始下載 100 張圖片!
其實從上方的圖片能看出來,畫面中能顯示的圖片數量是很有限的,頂多就那 6 張圖,如果使用者看完 6 張圖就離開了網站,那其他 94 張圖的資源就是被浪費掉了!
基於上面的情境,我們知道現在要做的事情就是讓圖片「按需加載」,也就是出現在畫面上的圖片才去拿資源,這樣就能為使用者節省非常多的資源,也很直接地提升我們網頁的體驗。
這時你的腦中突然就閃過了「Lazy Loading」的字眼,沒錯,就決定是你了!
快樂的 Coding 時光
確定了實作的方向後,就剩下開始動工啦!首先我們可以開啟現在的 Imager 看看他最原始的模樣:
為了證明加入「Lazy Loading」真的有解決問題,我在左上角加入一個計數器,當有 下載資源就會讓數字加 1,簡單明瞭。
通常決定了解決的方向後,我習慣會列出能想到的實作方法:
因為之前就有看過使用 Intersection Observer 實作 Lazy Loading 的文章,所以我已經掌握了相關的技巧以及知識,這時候我就會多一項選擇。
如果情況允許,也許使用套件也是個不錯的選擇,簡單、容易上手,但是否有人維護和是否有文檔也要列入考量,因為專案不一定之後由你維護,可不是每個人都懂這個套件怎麼使用啊!
經過一番折騰,我決定使用 Intersection Observer 自己實作,原因是用它來實現「Lazy Loading」真的很簡單,特別用套件來維護的話會讓專案的大小增加,不是很理想。
接著就來說明一下實作的部分吧!建議搭配已經準備好的範例一起服用。
首先看到 ,我們將 src 拿掉,因為 src 會觸發瀏覽器下載圖片資源,取而代之 src 會暫存在 data-src 以利之後使用。
// App.js
import React from "react";
import Image from "./components/Image";
import useCount from "./hooks/useCount";
import generateImages from "./utils/generateImages";
import { View, Title, ImageBlock, ImageCount } from "./style";
const images = generateImages({ count: 100 });
const App = () => {
const { count, addCount } = useCount();
return (
<View>
<ImageCount>圖片載入數量:{count}</ImageCount>
<Title>Imager</Title>
<ImageBlock>
{images.map(image => (
<Image
key={image.id}
data-src={image.src} // 暫存 src
className="lazy-image" // 需要加入 lazy loading 的標記
onLoad={addCount}
// 為了避免瀏覽器自動去下載資源,註解它
// src={image.src}
/>
)}
</ImageBlock>
</View>
);
};
export default App;
接著我們加入一個 useLazyLoading 的 hook,將有 lazy-image 標籤的 取出來並交給 Intersection Observer 做監聽:
// useLazyLoading.js
import { useEffect } from "react";
const useLazyLoading = () => {
useEffect(() => {
// entries : 所有被 observer 監聽的元件,即 <Image />
// observer: 監聽器
const callback = (entries, observer) => {
entries.forEach((entry) => {
// 如果監聽的元件目前在視野範圍內
if (entry.isIntersecting) {
const image = entry.target; // 取得 <Image />
image.setAttribute("src", image.dataset.src); // 將 data-src 塞入 src 讓瀏覽器觸發下載資源
image.removeAttribute("data-src"); // 移除 data-src
observer.unobserve(image); // 取消監聽 <Image />
}
});
};
const observer = new IntersectionObserver(callback);
const lazyImages = document.querySelectorAll(".lazy-image");
// 取得所有 <Image className="lazy-image" /> 並讓 observer 開始監聽
lazyImages.forEach((image) => observer.observe(image));
}, []);
};
export default useLazyLoading;
最後將 useLazyLoading 加入到我們的 App.js 之中就大功告成:
// App.js
import React from "react";
import Image from "./components/Image";
import useCount from "./hooks/useCount";
import useLazyLoading from "./hooks/useLazyLoading";
import generateImages from "./utils/generateImages";
import { View, Title, ImageBlock, ImageCount } from "./style";
const images = generateImages({ count: 100 });
const App = () => {
const { count, addCount } = useCount();
useLazyLoading(); // 加入 Lazy Loading
return (
<View>
<ImageCount>圖片載入數量:{count}</ImageCount>
<Title>Imager</Title>
<ImageBlock>
{images.map(image => (
<Image
key={image.id}
data-src={image.src}
className="lazy-image"
onLoad={addCount}
/>
)}
</ImageBlock>
</View>
);
};
export default App;
我們來看看加入了「Lazy Loading」的網頁:
現在畫面上能看到幾張圖片,下載資源的數量就是幾張,哎呀!這不是解決了嗎,恭喜你完成了一項驚人的創舉,現在我們可以拿著這些結果和比對的數據去交差了!
完美的交付了任務之後,站起身子,伸伸懶腰,美好的一天即將結束!收拾收拾準備下班囉。